#include <sstream>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <boost/lexical_cast.hpp>
#include "SSLClient.h"
#include "SSLCommon.h"
#include "../CommError.h"
#include "../../smtp/data/SMTPDefaults.h"

using namespace kc_sf;

// -------------------------------------------------------------- //

SSLClient::SSLClient ()
: cl_ctx( NULL ),
  cl_ssl( NULL )
{
	SSL_library_init();

	SSL_load_error_strings();
	ERR_load_BIO_strings();
    ERR_load_SSL_strings();
	OpenSSL_add_all_algorithms();

	// ustala kontekst
	cl_ctx = SSL_CTX_new( SSLv23_client_method() );
    if(cl_ctx == NULL)
		throw CommError( FailedInit, "Błąd inicjalizacji kontekstu SSL" );

	cl_ssl = SSL_new (cl_ctx);
}

// -------------------------------------------------------------- //

SSLClient::~SSLClient ()
{
	if( isConnected() )
		disconnect ();

	if( cl_ctx != NULL )
	{
		SSL_CTX_free( cl_ctx );
		cl_ctx = NULL;
	}
}

// -------------------------------------------------------------- //

void SSLClient::initSSLContext ()
{
	if ( !isConnected() )
		throw CommError( FailedConnection, "Nie można ustalić kontekstu SSL. Nie połączono z serwerem.");

	BIO *sbio = BIO_new_socket( cl_sock, BIO_NOCLOSE );
	BIO_set_nbio( sbio, 1 );
	SSL_set_bio( cl_ssl, sbio, sbio );

	SSL_set_connect_state( cl_ssl ); // client mode
	SSL_set_mode( cl_ssl, SSL_MODE_AUTO_RETRY );

	// pobiera zaufane ca
	int ca = SSL_CTX_load_verify_locations( cl_ctx, "./certs/ca_certificate.pem", "./certs/" );
	if( !ca )
		throwSSLError( FailedInit, ca, "Błąd przy odczytywaniu listy certyfikatów" );

	// ssl handshake
	int err = SSL_connect( cl_ssl );
	if (err <= 0)
		throwSSLError( FailedConnection, err, "SSL_connect" );

	// sprawdza certyfikat serwera
	int verify = SSL_get_verify_result( cl_ssl );
	if( verify != X509_V_OK )
	{
		std::string verify_str = boost::lexical_cast<std::string>( verify );
		throw CommError( FailedConnection, "Błąd weryfikacji certyfikatu hosta " + verify_str );
	}

	cl_logger << info << sender << cidOut( cl_cid ) << "Zainicjalizowano sesję SSL" << flush;

	// tworzy obiekt do komunikacji
	cl_comm = new SSLCommon( cl_sock, cl_ssl, MAX_LINE_LENGTH );
}

// -------------------------------------------------------------- //

void SSLClient::disconnect()
{
	if( cl_ssl )
	{
		int r = SSL_shutdown( cl_ssl );
		switch (r)
		{
			case 1:
				break; /* Success */
			case 0:
			case -1:
			default:
				// shutdown failed
				break;
		}

		SSL_free (cl_ssl);
		cl_ssl = NULL;
	}

    TCPClient::disconnect();

    if( cl_comm )
    {
    	delete cl_comm;
    	cl_comm = NULL;
    }
}

// -------------------------------------------------------------- //

void SSLClient::recvData( std::string &to, const std::string &until_str, int timeout )
{
	if( cl_comm == NULL )
		throw CommError( FailedConnection, "Nie połączono z serwerem" );

	if( cl_comm->recvData( to, until_str, timeout ) != Success )
	{
		disconnect ();
		throw CommError( FailedRead, "Błąd odczytu SSLClient::recvData()" );
	}
}

// -------------------------------------------------------------- //

void SSLClient::sendData( const std::string &what, int timeout )
{
	if( cl_comm == NULL )
		throw CommError( FailedConnection, "Nie połączono z serwerem" );

	if( cl_comm->sendData( what, timeout ) != Success )
	{
		disconnect ();
		throw CommError( FailedWrite, "Błąd zapisu SSLClient::sendData()" );
	}
}

// -------------------------------------------------------------- //

void SSLClient::throwSSLError( CommErrorType type, int err, const std::string &err_msg )
{
	std::stringstream err_stream;
	int err_no = SSL_get_error( cl_ssl, err );
	const std::string err_type =
		err_no == SSL_ERROR_ZERO_RETURN ? "SSL_ERROR_ZERO_RETURN"
		: err_no == SSL_ERROR_WANT_READ ? "SSL_ERROR_WANT_READ"
		: err_no == SSL_ERROR_WANT_WRITE ? "SSL_ERROR_WANT_WRITE"
		: err_no == SSL_ERROR_WANT_CONNECT ? "SSL_ERROR_WANT_CONNECT"
		: err_no == SSL_ERROR_WANT_ACCEPT ? "SSL_ERROR_WANT_ACCEPT"
		: err_no == SSL_ERROR_WANT_X509_LOOKUP ? "SSL_ERROR_WANT_X509_LOOKUP"
		: err_no == SSL_ERROR_SYSCALL ? "SSL_ERROR_SYSCALL"
		: err_no == SSL_ERROR_SSL ? "SSL_ERROR_SSL"
		: err_no == SSL_ERROR_NONE ? "SSL_ERROR_SSL"
		: boost::lexical_cast<std::string>( err_no );

	err_stream << "SSLClient: " << err_msg << "; " << err_type
			<< ": " << ERR_reason_error_string( ERR_get_error() );
	throw CommError(FailedConnection, err_stream.str() );
}
